import * as PIXI from 'pixi.js';
import { EventSystem } from '@pixi/events';
import Bump from './bump';
import Chronograph from './chronograph';

// eslint-disable-next-line no-underscore-dangle
delete PIXI.Renderer.__plugins.interaction;

interface ResourceImage {
  src: string;
  top: number;
  left: number;
  width: number;
  height: number;
}

interface Resources {
  // 掉落元素图片
  fallingItemElement: ResourceImage;
  // 障碍物
  fallingBlockElement: ResourceImage;
  // 接收掉落元素图片
  receiveElement: ResourceImage;
  // 得分 +1 图片
  scoreAddElement: ResourceImage;
}

interface CallbackListener {
  (n: number): void;
}

interface GameProps {
  /** 游戏运行容器元素 */
  el: null | HTMLElement;
  /** 所有资源 */
  resources: Resources;
  /** 限时 */
  timeLimit?: number;
  /** 速度等级 */
  speedLevel: SPEED_LEVEL;
  /** 时间变化监听 */
  timeListeners?: CallbackListener[];
  /** 得分变化监听 */
  scoreListeners?: CallbackListener[];
  /** 游戏结束 */
  gameOver?: ({ status: END_STATUS, score: number }) => void;
}

enum FALL_TYPES {
  ITEM = 'fallingItemElement',
  BLOCK = 'fallingBlockElement',
}

interface FallingSpriteItem {
  type: FALL_TYPES;
  sprite: PIXI.Sprite;
  speed: number;
}

/** 碰撞判断 */
const bump = new Bump(PIXI);

export enum STAGE {
  WAITING = 'waiting',
  RUNNING = 'running',
  PAUSE = 'pause',
  STOP = 'stop',
}

export enum SPEED_LEVEL {
  SLOW = 'slow',
  MIDDLE = 'middle',
  FAST = 'fast',
}

export enum END_STATUS {
  TIMEOUT = 'timeout',
  FAILED = 'failed',
}

/** 不同速度对设备高度等分, 动态获取速度 */
const SPEED_PIECE = {
  [SPEED_LEVEL.SLOW]: 110,
  [SPEED_LEVEL.MIDDLE]: 90,
  [SPEED_LEVEL.FAST]: 70,
};

const defaultResource = () => ({
  src: '',
  top: 0,
  left: 0,
  width: 0,
  height: 0,
});
const createDefaultProps = (): GameProps => ({
  el: null,
  resources: {
    fallingItemElement: defaultResource(),
    fallingBlockElement: defaultResource(),
    receiveElement: defaultResource(),
    scoreAddElement: defaultResource(),
  },
  timeLimit: 30,
  speedLevel: SPEED_LEVEL.MIDDLE,
  timeListeners: [],
  scoreListeners: [],
});

export class Game {
  /** pixi 实例 */
  private app: PIXI.Application = new PIXI.Application({
    antialias: true, // 消锯齿
    autoDensity: true,
    backgroundAlpha: 0,
    resolution: devicePixelRatio,
  });

  private $options: GameProps;

  /** 是否已经进行初始化 */
  private isInit = false;

  /** 当前游戏阶段 */
  private stage: STAGE = STAGE.WAITING;

  /** 计时器 */
  private chronograph: Chronograph = new Chronograph();

  /** 得分 */
  private score = 0;

  /** 速度 */
  private speed = 10;

  /** 增速基础值 */
  private increment = 2;

  /** 增速各阶段以及当前阶段使用增速倍数, 最后一个阶段无效 */
  private increments = [0, 1, 2, 3];

  private dragSprite?: PIXI.Sprite;

  /** 掉落元素: 掉落物 + 障碍物 */
  private fallingElements: FallingSpriteItem[] = [];

  /** 得分 +1 元素 */
  private plusElements: PIXI.Sprite[] = [];

  /** 得分元素追加定时器 */
  private itemTimer: NodeJS.Timeout | null = null;

  /** 障碍物追加定时器 */
  private blockTimer: NodeJS.Timeout | null = null;

  /** 生成粽子基础间隔时间 */
  private baseAppendTime = 300;

  /** 生成粽子变动时间 */
  private dynamicAppendTime = 300;

  /** 得分更新定时器 */
  private scoreListeners: CallbackListener[] = [];

  constructor(props: GameProps) {
    const options = { ...createDefaultProps(), ...props };
    this.$options = options;
    this.scoreListeners = options.scoreListeners as CallbackListener[];
    const timeListeners = options.timeListeners || [];
    timeListeners.forEach(fn => this.onTimeChange(fn));
    this.onTimeChange(this.handleGameOver);
  }

  /** 游戏初始化 */
  public init() {
    if (this.isInit) return;

    this.initGameSetting();
    this.initDragElement();
  }

  /** 发生 resize 时执行位置重新计算 */
  public resize() {
    this.setDragSpriteAxis();
  }

  /** 初始化游戏配置 */
  private initGameSetting() {
    const { app } = this;
    const { el, speedLevel } = this.$options;
    el?.appendChild(app.view);
    app.resizeTo = el as HTMLElement;
    app.resize();
    app.stage.interactive = true;
    if (!('events' in app.renderer) && 'addSystem' in app.renderer) {
      app.renderer.addSystem(EventSystem as any, 'events');
    }
    // 初始化速度
    const speed = this.getSpeed(speedLevel);
    // 初始化增速
    this.increment = (this.increment * speed) / this.speed;
    // 配置初始速度
    this.speed = speed;
  }

  /** 设置游戏阶段 */
  private setStage = (stage: STAGE) => {
    this.stage = stage;
  };

  /** 获取图片代理地址 */
  private createProxyImageSprite = (src: string) =>
    PIXI.Texture.from('');

  /** 初始化拖拽元素 */
  private initDragElement() {
    const resource = this.$options.resources.receiveElement;
    const texture = this.createProxyImageSprite(resource.src);
    const sprite = new PIXI.Sprite(texture);
    const viewSize = this.getViewSize();
    this.dragSprite = sprite;

    this.setDragSpriteAxis();

    sprite.interactive = true;

    sprite.addEventListener('pointerdown', this.onDragStart);
    sprite.addEventListener('pointerup', this.onDragEnd);
    sprite.addEventListener('pointerupoutside', this.onDragEnd);

    // 增强拖拽区域
    // eslint-disable-next-line no-case-declarations
    const rect = new PIXI.Sprite();
    rect.x = 0;
    rect.y = 0;
    rect.width = viewSize.width;
    rect.height = viewSize.height;
    this.app.stage.addChild(rect);
    // 位于上层
    this.app.stage.addChild(sprite);
  }

  /** 计算承接物坐标相关尺寸 */
  private setDragSpriteAxis() {
    const sprite = this.dragSprite;
    console.log(this);
    if (typeof sprite === 'undefined') return;

    const resource = this.$options.resources.receiveElement;
    const { left } = this.toRelativePosition(resource);
    const { width, height } = this.toRelativeSize(resource);
    const viewSize = this.getViewSize();
    const zoom = this.getZoom();

    sprite.width = width;
    sprite.height = height;

    sprite.y = viewSize.height - (height + 60 * zoom);
    sprite.x = left;
  }

  // /** 初始化游戏记录数据 */
  // private resetRecordData = () => {
  //   /* 重置得分 */
  //   this.score = 0;
  // };

  /** 接粽子拖拽 - 手指按下 */
  private onDragStart = () => {
    this.app.stage.addEventListener('pointermove', this.onDragMove);
  };

  /** 接粽子拖拽 - 结束 */
  private onDragEnd = () => {
    this.app.stage.removeEventListener('pointermove', this.onDragMove);
  };

  /** 接粽子拖拽 - 拖动 */
  private onDragMove = e => {
    if (this.dragSprite) {
      const { width } = this.dragSprite;
      const maxX = this.app.stage.width - width;
      let x = e.global.x - width / 2;
      x = x < 0 ? 0 : x > maxX ? maxX : x;
      this.dragSprite.position.x = x;
    }
  };

  /** 读取当前画布尺寸 */
  private getViewSize() {
    const { offsetWidth, offsetHeight } = this.$options.el as HTMLElement;

    return {
      width: offsetWidth,
      height: offsetHeight,
    };
  }

  /** 获取尺寸缩放比 */
  private getZoom(): number {
    const viewSize = this.getViewSize();
    const zoom = viewSize.width / 320;

    return zoom;
  }

  /** 转换到相对尺寸 */
  private toRelativeSize(resource: ResourceImage) {
    const zoom = this.getZoom();
    const width = Math.floor(zoom * resource.width);
    const height = Math.floor(zoom * resource.height);
    return { width, height };
  }

  /** 装换到相对位置 */
  private toRelativePosition(resource: ResourceImage) {
    const zoom = this.getZoom();
    const top = Math.floor(zoom * resource.top);
    const left = Math.floor(zoom * resource.left);
    return { top, left };
  }

  /** 更具速度等级获取速度 */
  private getSpeed(level: SPEED_LEVEL) {
    const { height } = this.getViewSize();
    return height / SPEED_PIECE[level];
  }

  /** 随机新增掉落物时间 */
  private getAppendTime() {
    // 获取"慢速"与"当前速度"比例, 比例越小, 表示当前速度越快
    const zoom = this.getSpeed(SPEED_LEVEL.SLOW) / this.speed;
    // 速度愉快, 生成掉落物间隔越短
    return Math.floor(
      (Math.random() * this.dynamicAppendTime + this.baseAppendTime) * zoom
    );
  }

  /** 获取当前阶段生成掉落物的速度 */
  private getCurSpeed() {
    const level = Math.floor(
      this.chronograph.getSecs() /
        ((this.$options.timeLimit as number) / this.increments.length)
    );
    const increment = this.increment * this.increments[level];

    return this.speed + increment;
  }

  /** 添加得分 +1 元素 */
  private createPlusSprite = (sprite: PIXI.Sprite) => {
    const { scoreAddElement } = this.$options.resources;
    const texture = this.createProxyImageSprite(scoreAddElement.src);
    const plus = new PIXI.AnimatedSprite([texture]);
    const { width, height } = this.toRelativeSize(scoreAddElement);
    plus.width = width;
    plus.height = height;
    plus.y = sprite.y;
    plus.x = Math.floor(sprite.x + (sprite.width - width) / 2);
    plus.animationSpeed = 0.2;
    plus.play();
    sprite.anchor.set(0.5);
    this.plusElements.push(plus);
    this.app.stage.addChild(plus);
  };

  /** 根据类型生成掉落 sprite(得分元素 + 障碍物) */
  private createFallingSprite = (type: FALL_TYPES) => {
    const { resources } = this.$options;

    const viewSize = this.getViewSize();
    const resource = resources[type];
    const texture = this.createProxyImageSprite(resource.src);
    const { width, height } = this.toRelativeSize(resource);
    const sprite = new PIXI.AnimatedSprite([texture]);
    const speed = this.getCurSpeed();

    sprite.width = width;
    sprite.height = height;
    sprite.x = Math.floor(Math.random() * (viewSize.width - width));
    sprite.y = -height;
    sprite.animationSpeed = 0.2;
    switch (type) {
      case FALL_TYPES.BLOCK:
        break;
      default:
        // FALL_TYPES.ITEM
        sprite.anchor.set(0.5);
        sprite.rotation = Math.PI / 2 - Math.PI * Math.random();
    }
    sprite.play();

    this.app.stage.addChild(sprite);
    this.fallingElements.push({
      type,
      sprite,
      speed,
    });

    return sprite;
  };

  /** 追加积分掉落物 */
  private appendItemSprite = () => {
    const type = FALL_TYPES.ITEM;
    this.createFallingSprite(type);
    const duration = this.getAppendTime();

    this.clearItemTimer();
    this.itemTimer = setTimeout(() => {
      this.appendItemSprite();
    }, duration);
  };

  /** 追加障碍物 */
  private appendBlockSprite = () => {
    this.clearBlockTimer();
    const duration = Math.floor(2000 + Math.random() * 4000);

    this.blockTimer = setTimeout(() => {
      const type = FALL_TYPES.BLOCK;
      this.createFallingSprite(type);
      this.appendBlockSprite();
    }, duration);
  };

  /** 开始掉落物品, app ticker 添加执行方法 */
  private startFallingSprite() {
    this.app.ticker.add(this.running);
  }

  /** 停止掉落物品, app ticker 移除执行方法 */
  private stopFallingSprite() {
    this.app.ticker.remove(this.running);
  }

  /** 心跳执行方法, 掉落物 Y 轴移动 */
  private running = (delta: number) => {
    if (this.stage !== STAGE.RUNNING) return;

    const viewSize = this.getViewSize();
    const { fallingElements, plusElements } = this;
    let score = 0;
    // 处理掉落元素(使用 for 遍历倒序遍历, 保证可以在遍历过程中进行元素移除)
    for (let i = fallingElements.length - 1; i >= 0; i--) {
      const element = fallingElements[i];
      if (bump.hit(element.sprite, this.dragSprite)) {
        if (element.type === FALL_TYPES.BLOCK) {
          this.stop(END_STATUS.FAILED);
        } else {
          score += 1;
          this.createPlusSprite(element.sprite);
          this.removeSprite(i);
        }
      } else if (element.sprite.y > viewSize.height) {
        this.removeSprite(i);
      } else {
        element.sprite.y += element.speed * delta;
      }
    }
    if (score > 0) {
      this.addScore(score);
    }
    // 处理 +1 元素
    // if (!plusElements.length) return;

    for (let i = plusElements.length - 1; i >= 0; i--) {
      const plus = plusElements[i];
      if (plus.alpha <= 0) {
        this.removePlus(i);
      } else {
        plus.alpha -= 0.02 * delta;
        plus.y -= 0.005 * delta;
      }
    }
  };

  private addScore = (n: number) => {
    this.score += n;
    this.scoreListeners.forEach(fn => fn(this.score));
  };

  /** 移除 sprite */
  private removeSprite = (index: number) => {
    const { fallingElements } = this;
    const { sprite } = fallingElements[index];
    fallingElements.splice(index, 1);
    this.app.stage.removeChild(sprite);
    sprite.destroy();
  };

  /** 移除 +1 元素 */
  private removePlus = (index: number) => {
    const { plusElements } = this;
    const sprite = plusElements[index];
    plusElements.splice(index, 1);
    this.app.stage.removeChild(sprite);
    sprite.destroy();
  };

  /** 清理物品掉落定时器 */
  private clearItemTimer = () => {
    if (this.itemTimer) {
      clearTimeout(this.itemTimer);
      this.itemTimer = null;
    }
  };

  /** 清理物品掉落定时器 */
  private clearBlockTimer = () => {
    if (this.blockTimer) {
      clearTimeout(this.blockTimer);
      this.blockTimer = null;
    }
  };

  /** 启动游戏 */
  public start = () => {
    this.setStage(STAGE.RUNNING);
    this.chronograph.start();
    this.appendItemSprite();
    this.appendBlockSprite();
    this.startFallingSprite();
  };

  /** 暂停游戏 */
  public pause = () => {
    this.setStage(STAGE.PAUSE);
    this.chronograph.pause();
    this.clearItemTimer();
    this.clearBlockTimer();
    this.stopFallingSprite();
  };

  /** 继续游戏 */
  public continue = () => {
    if (this.stage === STAGE.RUNNING) return;

    this.setStage(STAGE.RUNNING);
    this.chronograph.continue();
    this.appendItemSprite();
    this.appendBlockSprite();
    this.startFallingSprite();
  };

  /** 游戏结束判断 */
  public handleGameOver = time => {
    if (time >= (this.$options.timeLimit as number)) {
      this.stop(END_STATUS.TIMEOUT);
    }
  };

  /** 游戏结束, 停止游戏 */
  public stop = (status: END_STATUS) => {
    this.clearItemTimer();
    this.clearBlockTimer();

    // 结束游戏, 停止定时器
    this.chronograph.stop();
    this.setStage(STAGE.STOP);
    this.$options.gameOver &&
      this.$options.gameOver({ status, score: this.score });
  };

  // /** 重开游戏 */
  // public restart = () => {
  //   this.resetRecordData();
  //   this.chronograph.reset();
  //   this.scoreListeners.forEach(fn => fn(this.score));
  // };

  public getScore = () => this.score;

  public isRunning = () => this.stage === STAGE.RUNNING;

  /** 添加时间变更监听函数 */
  public onTimeChange = (fn: CallbackListener) => {
    this.chronograph.addTick(fn);
  };

  /** 添加得分变更监听函数 */
  public onScoreChange = (fn: CallbackListener) => {
    this.scoreListeners.push(fn);
  };

  /** 销毁 */
  public destroy = () => {
    const { dragSprite } = this;

    this.clearItemTimer();
    this.clearBlockTimer();

    dragSprite?.removeEventListener('pointerdown', this.onDragStart);
    dragSprite?.removeEventListener('pointerup', this.onDragEnd);
    dragSprite?.removeEventListener('pointerupoutside', this.onDragEnd);

    this.scoreListeners = [];
    this.chronograph.destroy();
    this.app.destroy();
  };
}

export const createCatchZongZi = (options: GameProps): Game => {
  const game = new Game(options);
  game.init();
  return game;
};
